package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket
        运行在服务端，主要作用有两个
        1:向系统申请服务端口，客户端就可以通过这个端口与服务端建立链接
        2:监听服务端口，一旦客户端链接就创建一个Socket与该客户端交互
        如果将Socket比喻为"电话",那么ServerSocket相当于是"总机"
     */
    private ServerSocket serverSocket;
    //存放所有客户端输出流，用于广播消息
//    private List<PrintWriter> allOut = new ArrayList<>();
    /*
        存放所有客户端的输出流
        key:该客户端的昵称
        value:对应该客户端的输出流
     */
    private Map<String,PrintWriter> allOut = new HashMap<>();

    //线程池
    private ExecutorService threadPool;

    public Server(){
        try {
            System.out.println("正在启动服务端...");
            /*
                创建ServerSocket对象时，会申请一个端口
                如果该端口被其他程序占用，则会抛出异常:
                java.net.BindException: Address already in use: JVM_Bind
             */
            serverSocket = new ServerSocket(8088);
            threadPool = Executors.newFixedThreadPool(50);
            System.out.println("服务端启动成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端链接...");
                /*
                Socket accept()
                用于接受客户端的链接。
                该方法是一个阻塞方法(方法调用后就会"卡住")，直到客户端
                链接为止，该方法会返回一个Socket对象，这个对象就与客户端
                的Socket进行了对等链接，用于双向通讯。
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端链接了!");
                //启动一个线程来处理该客户端交互
                ClientHandler handler = new ClientHandler(socket);
//                Thread t = new Thread(handler);
//                t.start();
                threadPool.execute(handler);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务用于负责与指定的客户端交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录该客户端的ip地址
        private String nickname;//客户端的昵称

        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取远端计算机地址信息，对于服务端这边而言，远端就是客户端
            host = socket.getInetAddress().getHostAddress();
        }
        @Override
        public void run() {
            PrintWriter pw = null;
            try {
                /*
                    Socket提供的方法:
                    InputStream getInputStream()
                    通过socket获取的输入流可以读取对方发送过来的数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //首先单独读取来自客户端发送过来的一行字符串，为该客户端的昵称
                nickname = br.readLine();


                //通过socket获取输出流用于将消息发送给客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //向集合中添加元素要添加同步机制，保证多个线程不能同时添加
                synchronized (allOut) {
                    //将该客户端的输出流存入共享集合
//                    allOut.add(pw);
                    allOut.put(nickname,pw);
                }

                //通知所有客户端该用户上线了
                sendMessage(nickname+"["+host+"]上线了,当前在线人数:"+allOut.size());


                //读取客户端发送过来的一行字符串
                String message;
                /*
                    利用BufferedReader的readLine方法读取对方发送过来的一行
                    字符串时，如果对方正确的断开链接(进行四次挥手操作)那么此时
                    会返回null。
                    如果对方未正确断开，此时会抛出异常
                 */
                while ((message = br.readLine()) != null) {
                    //如果聊天内容以"@"开始则为私聊功能
                    if(message.startsWith("@")){
                        //message的样子:@王克晶:你好！
                        sendMessageToSomeone(message);
                    }else {//否则为群聊
                        sendMessage(nickname + "[" + host + "]说:" + message);
                        saveMessage(nickname,null,message);
                    }
                }
            } catch (IOException e) {

            } finally {
                //处理客户端下线后的操作
                //1将该客户端的输出流从共享集合中删除
                synchronized (allOut) {
//                    allOut.remove(pw);
                    allOut.remove(nickname);
                }
                //2通知所有客户端该用户下线了
                sendMessage(nickname+"["+host+"]下线了,当前在线人数:"+allOut.size());
                try {
                    //3关闭socket
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将消息发送给指定用户(私聊功能)
         * @param message
         */
        private void sendMessageToSomeone(String message){
            //message的样子:@王克晶:你好！
            //验证消息格式是否正确
            if(message.matches("@.+:.+")){
                //提取对方的昵称
                String toNickname = message.substring(1,message.indexOf(":"));
                //判断对方是否在线
                if(allOut.containsKey(toNickname)) {
                    //根据对方的昵称获取对方的输出流，用于将消息发送给他
                    PrintWriter pw = allOut.get(toNickname);
                    //通过message截取出私聊内容
                    String toMessage = message.substring(message.indexOf(":")+1);
                    pw.println(nickname+"悄悄对你说:"+toMessage);
                    //将该消息保存到数据库中
                    saveMessage(nickname,toNickname,toMessage);

                }else{//对方不存在应当通知当前用户
                    PrintWriter pw = allOut.get(nickname);
                    pw.println("该用户["+toNickname+"]不存在!");
                }

            }else{//格式不正确，通知当前用户格式不对
                //用当前客户端的昵称获取输出流
                PrintWriter pw = allOut.get(nickname);
                pw.println("私聊格式不正确!");
            }
        }

        /**
         * 将消息发送给所有客户端
         * @param message
         */
        private void sendMessage(String message){
            System.out.println(message);
            synchronized (allOut) {
                /*
                    新循环遍历集合就是迭代器，因为迭代器要求遍历过程中不可以用集合方法
                    增删元素，因此为了保证遍历正确，防止其他线程增删，这里要和其他线程
                    的增删动作互斥
                 */
//                for (PrintWriter o : allOut) {
//                    o.println(message);
//                }
                for (PrintWriter o : allOut.values()) {
                    o.println(message);
                }
            }
        }

        private void saveMessage(String fromUser,String toUser,String info){
            try (
                    Connection connection = DBUtil.getConnection();
            ){
                String sql = "INSERT INTO chatinfo(from_user,to_user,info) " +
                             "VALUES(?,?,?)";
                PreparedStatement ps = connection.prepareStatement(sql);
                ps.setString(1,fromUser);
                ps.setString(2,toUser);
                ps.setString(3,info);
                ps.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }

}







